package net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea.algorithms.algorithmModules

import net.cyndeline.scalarlib.rldungeon.dgs.graph.{DGSFunctionality, Room}
import scalax.collection.GraphEdge.UnDiEdge
import scalax.collection.GraphPredef.OuterEdge
import scalax.collection.immutable.Graph
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.help.{SuperGraphVertexReplacer, SuperGraphVertexReplacerInterface, CollapsedEdge, CollapsedNode}
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea._
import com.escalatesoft.subcut.inject.{Injectable, BindingModule}
import scala.reflect.runtime.universe._
import scala.reflect._
import scala.util.Random
import net.cyndeline.scalarlib.rldungeon.dgs.ActivatorResponderProcessor
import net.cyndeline.scalarlib.rlgraph.util.{EdgeCopyFactory, VertexReplacer}
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea.help.{RandomRepresentedVertexFinder, MaximumDistancePointlessNode, EdgesBelowNodes, NodeStorage}
import net.cyndeline.scalarlib.util.{RandomElementFinderInterface, RandomElementFinder}

/**
 * Used for dependency injection.
 */
trait SingleResponderAppenderI {
  def apply[VType <: Room[VType] : TypeTag : ClassTag, EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
           (regularGraph: Graph[VType, EType],
            collapsedGraph:Graph[CollapsedNode[VType], CollapsedEdge[VType]#Inner],
            storage: Map[CollapsedNode[VType], NodeStorage[CollapsedNode[VType]]],
            areaConnections: Set[CollapsedNode[VType]],
            edgeMap: EdgesBelowNodes[VType],
            activatorResponderProcessor: ActivatorResponderProcessor[VType, EType],
            edgeFactory: EdgeCopyFactory[VType, EType]): Option[ResponderResult[VType, EType]]
}

/**
 * The result of the responder appenders apply method.
 *
 * @constructor Create a new responder appendage result.
 * @param regularGraph The supplied regular graph, now with the vertex/edge pair that hosts the activator/responder.
 * @param collapsedGraph The collapsed graph, updated with the vertex holding the activator. The node is still set as
 *                       pointless however.
 * @param storage An updated map with the new collapsed node mapped to the previous node's container.
 * @param updatedNode The updated node with the activator-vertex in it.
 * @param oldNode The node that was updated (no longer present in the collapsed graph).
 * @param mainAreaConnection The node on the main path that the updated nodes pointless area connects to.
 * @param updatedEdge The updated regular edge with the responder in it.
 * @param updatedEdgeStorage The storage tuple representing the collapsed nodes hosting both ends of the regular edge
 *                           that has the responder in it.
 * @tparam VType Type of vertex represented by the collapsed nodes.
 * @tparam EType Type of edge being represented by the collapsed edges.
 */
final case class ResponderResult[VType <: Room[VType], EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
  (regularGraph: Graph[VType, EType],
   collapsedGraph: Graph[CollapsedNode[VType], CollapsedEdge[VType]#Inner],
   storage: Map[CollapsedNode[VType], NodeStorage[CollapsedNode[VType]]],
   updatedNode: CollapsedNode[VType],
   oldNode: CollapsedNode[VType],
   mainAreaConnection: CollapsedNode[VType],
   updatedEdge: EType[VType],
   updatedEdgeStorage: (NodeStorage[CollapsedNode[VType]], NodeStorage[CollapsedNode[VType]]))

/**
 * Adds activator/responder pairs to a graph.
 *
 * @constructor Creates a new appender module.
 */
class SingleResponderAppender(random: Random)(implicit val bindingModule: BindingModule)
  extends Injectable
  with SingleResponderAppenderI {

  /* Selects a random element from a set. */
  private val randomElementFinder = injectOptional[RandomElementFinderInterface] getOrElse { new RandomElementFinder() }

  /* Replaces represented vertices inside a cycle-collapsed graph. */
  private val superNodeReplacer = injectOptional[SuperGraphVertexReplacerInterface] getOrElse { new SuperGraphVertexReplacer() }

  /**
   * Takes a graph and its cycle-collapsed counterpart and adds a single activator and corresponding responder to it.
   * @param regularGraph Graph to add activator/responder pair to.
   * @param collapsedGraph Cycle-collapsed version of the original graph. The nodes in this does not have to contain
   *                       the entire regular graph, it is enough if they represent as subset of the regular edges,
   *                       as trimming away vertices that cannot host activators cuts down on node iteration.
   * @param storage Maps every collapsed node to its mutable storage container.
   * @param areaConnections A set of all nodes on the main path in the collapsed graph that has edges below them.
   * @param edgeMap Maps the edges that appears below each node on the main path to the node itself.
   * @param activatorResponderProcessor Used to update the regular vertices and edges with activators and responders.
   * @param edgeFactory Used when updating the regular graph.
   * @return See the container class for a closer description.
   */
  def apply[VType <: Room[VType] : TypeTag : ClassTag, EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
           (regularGraph: Graph[VType, EType],
            collapsedGraph:Graph[CollapsedNode[VType], CollapsedEdge[VType]#Inner],
            storage: Map[CollapsedNode[VType], NodeStorage[CollapsedNode[VType]]],
            areaConnections: Set[CollapsedNode[VType]],
            edgeMap: EdgesBelowNodes[VType],
            activatorResponderProcessor: ActivatorResponderProcessor[VType, EType],
            edgeFactory: EdgeCopyFactory[VType, EType]): Option[ResponderResult[VType, EType]] = {

    /* Finds a candidate node for activators to be placed on. */
    val pointlessNodeFinder = injectOptional[PointlessNodeFinder[VType]] getOrElse { new MaximumDistancePointlessNode[VType](random) }

    /* Used to select which vertex should receive an activator if the selected node representes multiple vertices. */
    val vertexFinder = injectOptional[RepresentedVertexFinder[VType]] getOrElse { new RandomRepresentedVertexFinder[VType](random) }

    val vertexReplacer = new VertexReplacer[VType, EType](edgeFactory)
    val pointlessNode: Option[CollapsedNode[VType]] = pointlessNodeFinder.findActivatorNode(collapsedGraph, areaConnections)

    if (pointlessNode.isDefined) {

      /* Find which node the pointless node connects to on the main path, and select a random meaningful edge below
       * that node.
       */
      val n = pointlessNode.get // The node that should get an activator
      val mainAreaConnection = n.mainAreaConnection
      val parentOnMainPath = findMainAreaConnectionNode(mainAreaConnection, areaConnections)
      val responderEdgeCandidates = edgeMap.edgesBelow(storage(parentOnMainPath))
      val edgeStorage = randomElementFinder.findRandomElement(responderEdgeCandidates, random)

      /* The original vertex and edge from the non-collapsed graph. */
      val representedVertex = findRepresentedActivatorVertex(n, vertexFinder)
      val representedEdge = findRepresentedEdge(edgeStorage, regularGraph, collapsedGraph)
      val updatedVertexAndEdge = activatorResponderProcessor.createActivatorResponder(representedVertex, representedEdge, regularGraph)
      val updatedVertex = updatedVertexAndEdge._1
      val updatedEdge = updatedVertexAndEdge._2

      /* The edge must be replaced first, otherwise it brings the old vertex back into the graph. */
      var currentGraph = regularGraph
      currentGraph -= representedEdge
      currentGraph += updatedEdge
      currentGraph = vertexReplacer.replace(representedVertex, updatedVertex, currentGraph)

      val replacedGraphAndNodes = superNodeReplacer.replaceVertexSetUsingNode(Map(representedVertex -> updatedVertex), n, collapsedGraph)

      val currentCollapsedGraph = replacedGraphAndNodes._1
      val updatedCollapsedNode = replacedGraphAndNodes._2
      val oldCollapsedNode = n
      val updatedStorage = replaceNodeInStorage(oldCollapsedNode, updatedCollapsedNode, storage)

      Option(ResponderResult[VType, EType](currentGraph, currentCollapsedGraph, updatedStorage, updatedCollapsedNode, oldCollapsedNode, parentOnMainPath, updatedEdge, edgeStorage))

    } else {
      None
    }
  }

  /**
   * Retrieves the vertex represented by a collapsed node capable of holding an activator.
   * If multiple vertices exist, one is chosen at random.
   */
  private def findRepresentedActivatorVertex[VType <: Room[VType], EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
                                            (node: CollapsedNode[VType],
                                             vertexFinder: RepresentedVertexFinder[VType]): VType = {
    val v = if (node.isSuperNode) {
      vertexFinder.findVertexThatHoldsActivators(node).getOrElse {
        throw new Error("The node " + node + " was selected by the node finder as a candidate for an activator, but it contains no vertices capable of storing it.")
      }
    } else {
      node.singleRepresentedVertex
    }

    if (!v.canHoldAdditionalActivators)
      throw new Error("The single represented vertex " + v + " was chosen to hold an activator, but cannot.")
    else
      v
  }

  /**
   * Finds a represented edge from two collapsed nodes by first finding the collapsed edge corresponding to them,
   * then finding the edges original targets and using them to retrieve the original edge.
   */
  private def findRepresentedEdge[VType <: Room[VType], EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
                                 (edge: (NodeStorage[CollapsedNode[VType]], NodeStorage[CollapsedNode[VType]]),
                                  graph: Graph[VType, EType],
                                  collapsedGraph: Graph[CollapsedNode[VType], CollapsedEdge[VType]#Inner]): EType[VType] = {
    val collapsedNodes = (edge._1.node, edge._2.node)
    val collapsedEdge = collapsedGraph.edges.toOuter.find(e => e._1 == collapsedNodes._1 && e._2 == collapsedNodes._2).getOrElse {
      throw new IllegalArgumentException("No edge with the nodes " + edge + " was found in the graph " + collapsedGraph)
    }

    val originalTargets = CollapsedEdge.targetsOfEdge(collapsedEdge)
    val originalEdge = graph.edges.toOuter.find(e => e._1 == originalTargets._1 && e._2 == originalTargets._2).getOrElse {
      throw new IllegalArgumentException("No edge with the vertices " + originalTargets + " was found in the graph " + graph)
    }

    originalEdge
  }

  private def replaceNodeInStorage[VType <: Room[VType], EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
                                  (oldN: CollapsedNode[VType],
                                   newN: CollapsedNode[VType],
                                   store: Map[CollapsedNode[VType], NodeStorage[CollapsedNode[VType]]]
                                    ): Map[CollapsedNode[VType], NodeStorage[CollapsedNode[VType]]] = {
    val storage = store(oldN)
    storage.node = newN
    store - oldN + (newN -> storage)
  }

  private def findMainAreaConnectionNode[VType <: Room[VType], EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
                                        (vertex: VType, areaConnections: Set[CollapsedNode[VType]]): CollapsedNode[VType] = {
    areaConnections.find(n => n.contains(vertex)).getOrElse {
      throw new Error("No node representing " + vertex + " found in the set of main path connections: " + areaConnections)
    }
  }
}
